#include "rendercontext.h"
#include "viewport.h"
#include "vcache.h"
#include "immediate.h"

#include "pixel_buffer.h"

#include "OpenGL.h"

#include "../window.h"
#include "../timer.h"

#ifdef HAS_D3D
bool UseOpenGL = true;
////bool UseOpenGL = false;
#endif

void
e3dRenderContextAPI::destroy (void)
{
  if (_driver != NULL) {
    delete _driver;
    _driver = NULL;
    //_driver->destroy ();
  }
}

bool
e3dOpenGL::RenderContext::create (eWindow* win, e3dRenderContext* base)
{
  _base = base;

#ifdef WIN32
  _imp = new Win32::RenderContext  ();
#else
#ifndef __APPLE__
  _imp = new X11::RenderContext    ();
#else
  _imp = new Quartz::RenderContext ();
#endif
#endif

  if (! _imp)
    return false;
  
  return _imp->create (win, base);
}

void
e3dOpenGL::RenderContext::destroy (void)
{
  e3dRenderContextAPI::destroy ();

  if (_imp != NULL) {
    _imp->destroy ();

    delete _imp;
    _imp = NULL;
  }
}

void
e3dOpenGL::RenderContext::begin_frame (void)
{
  if (_imp != NULL)
  {
    _imp->begin_frame ();
  }
}

void
e3dOpenGL::RenderContext::end_frame (void)
{
  if (_imp != NULL)
  {
    _imp->end_frame ();
  }
}

e3dPixelBuffer*
e3dOpenGL::RenderContext::get_pixels (int x, int y, int num)
{
  if (_imp != NULL)
  {
///    if (r_verbose)
///      printf ("get_pixels (...)\n");
    return _imp->get_pixels (x, y, num);
  }

  return NULL;
}

void
e3dOpenGL::RenderContext::resize (int width, int height)
{
  if (_imp != NULL)
  {
    _imp->resize (width, height);
  }
}

#include "texture.h"

e3dTexture*
e3dOpenGL::RenderContext::create_texture_2d (eImage* img, bool mipmap)
{
  Epsilon3D::Texture2D* tex = new
    Epsilon3D::OpenGL::Texture2D ();

  tex->create (img, mipmap);

  return new e3dTexture (tex);
}

e3dVertexCache*
e3dOpenGL::RenderContext::create_vertex_cache (void)
{
  e3dVertexBuffer* vb = new
    e3dOpenGL::VertexBuffer ();

  return new e3dVertexCache (vb);
}

e3dIndexCache*
e3dOpenGL::RenderContext::create_index_cache (void)
{
  e3dIndexBuffer* ib = new
    e3dOpenGL::IndexBuffer ();

  return new e3dIndexCache (ib);
}


bool
e3dRenderContext::Create (eWindow* win)
{
  if (! _imp) {
#ifndef HAS_D3D
    _imp = new e3dOpenGL::RenderContext ();
#else
    if (UseOpenGL) {
      _imp = new e3dOpenGL::RenderContext ();
    } else {
      _imp = new e3dDirect3D::RenderContext ();
    }
#endif

    if (! _timer)
      _timer = new eFrameTimer ();
    
    return _imp->create (win, this);
  }
  
  Assert (_imp == NULL, _T ("Attempting to create a Render Context twice..."));
  return false;
}

void
e3dRenderContext::Destroy (void)
{
  if (_imp != NULL) {
    _imp->destroy ();

    delete _imp;
    _imp = NULL;
  }

  if (_timer != NULL) {
    delete _timer;
    _timer = NULL;
  }
}

void
e3dRenderContext::BeginFrame (void)
{
  if (_imp != NULL) {
    if (_timer)
      _timer->Begin ();

    _imp->begin_frame ();
  }
}

void
e3dRenderContext::EndFrame (void)
{
  if (_imp != NULL) {
    _imp->end_frame ();

    if (_timer)
      _timer->End ();
  }
}

e3dPixelBuffer*
e3dRenderContext::GetPixels (int x, int y, int num)
{
  if (_imp != NULL) {
    return _imp->get_pixels (x, y, num);
  }

  return NULL;
}

void
e3dRenderContext::Resize (int width, int height)
{
  if (_imp != NULL) {
    _imp->resize (width, height);
  }
}

float
e3dRenderContext::GetFPS (void)
{
  if (_timer != NULL)
    return _timer->FPS ();

  return 0.0f;
}

e3dTexture*
e3dRenderContext::CreateTexture2D (eImage* img, bool mipmap)
{
  if (_imp != NULL)
    return _imp->create_texture_2d (img, mipmap);

  return NULL;
}

e3dVertexCache*
e3dRenderContext::CreateVertexCache (void)
{
  if (_imp != NULL)
    return _imp->create_vertex_cache ();

  return NULL;
}

e3dIndexCache*
e3dRenderContext::CreateIndexCache (void)
{
  if (_imp != NULL)
    return _imp->create_index_cache ();

  return NULL;
}


#ifdef WIN32
HGLRC e3dOpenGL::Win32::RenderContext::hrc_base = NULL;

bool
e3dOpenGL::Win32::RenderContext::create (eWindow* win, e3dRenderContext* base)
{
  _base = base;
  hWnd  = eHWND (win->getNativeWindowHandle ());

  static  PIXELFORMATDESCRIPTOR pfd =
  {
    sizeof (PIXELFORMATDESCRIPTOR),
    1,
    PFD_DRAW_TO_WINDOW |
    PFD_SUPPORT_OPENGL |
    PFD_DOUBLEBUFFER,
    PFD_TYPE_RGBA,
    32,                     // Color Depth (in bits)
    0, 0, 0, 0, 0, 0,       // Color Bits Ignored
    0,                      // No Alpha Buffer
    0,                      // Shift Bit Ignored
    0,                      // No Accumulation Buffer
    0, 0, 0, 0,             // Accumulation Bits Ignored
    24,                     // 16Bit Z-Buffer (Depth Buffer)
    0,                      // No Stencil Buffer
    0,                      // No Auxiliary Buffer
    PFD_MAIN_PLANE,         // Main Drawing Layer
    0,                      // Reserved
    0, 0, 0                 // Layer Masks Ignored
  };

  hDC         = GetDC             (hWnd);
  PixelFormat = ChoosePixelFormat (hDC, &pfd);

  SetPixelFormat (hDC, PixelFormat, &pfd);

  hRC = wglCreateContext (hDC);

  if (! e3dOpenGL::Win32::RenderContext::hrc_base) {
    e3dOpenGL::Win32::RenderContext::hrc_base = hRC;
  } else {
#ifdef NVIDIA_BUG_FIXED
    // Copy the base render context's attrib stack to our NEW render context.
    wglCopyContext (hrc_base, hRC, GL_ALL_ATTRIB_BITS);
#endif

    // Begin sharing display lists between base and primary render contexts...
    wglShareLists (hrc_base, hRC);
  }

  wglMakeCurrent (hDC, hRC);

  /* Load all extensions... */
  if (  glewInit () != GLEW_OK /*||
       wglewInit () != GLEW_OK */)
    return false;

  /** TODO: Add a method to e3dRenderContext to control VSYNC... **/
  // Disable VSYNC for now.
  wglSwapIntervalEXT (0);

  return true;
}

void
e3dOpenGL::Win32::RenderContext::destroy (void)
{
  wglMakeCurrent   (NULL, NULL);
  wglDeleteContext (hRC);
}

//
// Changing render states before calling begin_frame (...) or
//  after calling end_frame (...) is invalid; the behavior is
//  intentionally undefined.
//
void
e3dOpenGL::Win32::RenderContext::begin_frame (void)
{
#ifdef _MULTIPLE_WINDOWS
  wglMakeCurrent (hDC, hRC);
#endif

  _base->Clear (e3dRenderStates::ColorBuffer | e3dRenderStates::DepthBuffer);
}

void
e3dOpenGL::Win32::RenderContext::end_frame (void)
{
  SwapBuffers    (hDC);

#ifdef _MULTIPLE_WINDOWS
  wglMakeCurrent (NULL, NULL);
#endif
}

e3dPixelBuffer*
e3dOpenGL::Win32::RenderContext::get_pixels (int x, int y, int num)
{
  e3dPixelBuffer* out = new e3dPixelBuffer ();

  out->w   = 1;
  out->h   = 1;
  out->bpp = 4;

  out->data = (char *)new int;

  glReadPixels (x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, out->data);

  return out;
}

/// @TODO: Implement for OpenGL
void
e3dOpenGL::Win32::RenderContext::resize (int width, int height)
{
  return;
}
#endif /* WIN32 */


#ifdef __APPLE__
#include "AGL/agl.h"

bool
e3dOpenGL::Quartz::RenderContext::create (eWindow* win, e3dRenderContext* base)
{
/* OS X 10.5+ */
#ifdef AGL_VERSION_3_0
  GLint    attribs [] =
#else
  long int attribs [] =
#endif
                        { AGL_RGBA,        AGL_DOUBLEBUFFER,
                          AGL_ACCELERATED,
                          AGL_DEPTH_SIZE,  32,
                          AGL_STEREO,      AGL_NO_RECOVERY,
                            NULL };


  eWindowHandle handle = win->getNativeWindowHandle ();
  window = (WindowRef)(DWORD)handle;

/* OS X 10.5+ */
#ifdef AGL_VERSION_3_0
  pix_fmt = aglCreatePixelFormat (attribs);
#else
  pix_fmt = aglChoosePixelFormat (NULL, 0, attribs);
#endif

  if (pix_fmt != NULL)
  {
    context = aglCreateContext (pix_fmt, NULL);

#define WINDOWED 1
#if WINDOWED
/* OS X 10.5+ */
#ifdef AGL_VERSION_3_0 
    aglSetWindowRef      (context, window);
#else
    aglSetDrawable       (context, GetWindowPort (window));
#endif
#else
    aglSetFullScreen     (context, 1920, 1200, 60, 0);
    aglSetWindowRef      (context, window);
    resize               (1920, 1200);
#endif

    aglSetCurrentContext (context);

    return true;
  }

  printf ("Could not find a pixel format!\n");
  exit (-1);

  return false;
}

void
e3dOpenGL::Quartz::RenderContext::destroy (void)
{
}

//
// Changing render states before calling begin_frame (...) or
//  after calling end_frame (...) is invalid; the behavior is
//  intentionally undefined.
//
void
e3dOpenGL::Quartz::RenderContext::begin_frame (void)
{
  aglSetCurrentContext (context);
	/// XXX: TODO
}

void
e3dOpenGL::Quartz::RenderContext::end_frame (void)
{
  aglSwapBuffers (context);
////  aglSetCurrent (NULL);
}

e3dPixelBuffer*
e3dOpenGL::Quartz::RenderContext::get_pixels (int x, int y, int num)
{
  e3dPixelBuffer* out = new e3dPixelBuffer ();

  out->w   = 1;
  out->h   = 1;
  out->bpp = 4;

  out->data = (char *)new int;

  glReadPixels (x, y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, out->data);

  return out;
}

/// AGL needs to know when a window changes size
void
e3dOpenGL::Quartz::RenderContext::resize (int width, int height)
{
  Rect rectPort;

  GetWindowPortBounds (window, &rectPort);
  InvalWindowRect     (window, &rectPort);

  aglUpdateContext (context);
  return;
}
#endif



#ifdef HAS_D3D
LPDIRECT3D9          e3dDirect3D::d3d = NULL;
e3dDirect3D::Device* e3dDirect3D::dev = NULL;

static D3DXMATRIX matRotationX, matRotationY, matRotationZ;
static D3DXMATRIX matTranslation;
#endif

void e3dRotatef (float mag, float x, float y, float z)
{
#ifdef HAS_D3D
  if (UseOpenGL) {
#endif
    glRotatef (mag, x, y, z);
#ifdef HAS_D3D
  } else {
    D3DXMatrixRotationX   (&matRotationX, D3DXToRadian (mag) *  x);
    D3DXMatrixRotationY   (&matRotationY, D3DXToRadian (mag) *  y);
    D3DXMatrixRotationZ   (&matRotationZ, D3DXToRadian (mag) * -z);

    LPDIRECT3DDEVICE9 dev = e3dDirect3D::GetDev ();

    dev->SetTransform (D3DTS_WORLD, &(matRotationX * matRotationY * matRotationZ * matTranslation));
  }
#endif
}

void e3dTranslatef (float x, float y, float z)
{
#ifdef HAS_D3D
  if (UseOpenGL) {
#endif
    glTranslatef (x, y, z);
#ifdef HAS_D3D
  } else {
    D3DXMatrixTranslation (&matTranslation, x, y, -z);
  }
#endif
}

void e3dLoadIdentity (void)
{
#ifdef HAS_D3D
  if (UseOpenGL) {
#endif
    glLoadIdentity ();
#ifdef HAS_D3D
  } else {
    LPDIRECT3DDEVICE9 dev = e3dDirect3D::GetDev ();

    D3DXMatrixIdentity (&matTranslation);
		dev->SetTransform  (D3DTS_WORLD, &matTranslation);
  }
#endif
}

void SetupView (e3dViewport* viewport)
{
#ifdef HAS_D3D
  if (UseOpenGL) {
#endif
    glViewport       (0,                  0,
                      viewport->Width (), viewport->Height ());

    /*
    glScissor        (0,                  0,
                      viewport->Width (), viewport->Height ());
    glEnable         (GL_SCISSOR_TEST);
    */

    glMatrixMode     (GL_PROJECTION);
    e3dLoadIdentity  ();

    gluPerspective   (45.0f, viewport->AspectRatio (), 0.1f, 40000.0f);

    glMatrixMode     (GL_MODELVIEW);
    e3dLoadIdentity  ();
#ifdef HAS_D3D
  } else {
    LPDIRECT3DDEVICE9 dev = e3dDirect3D::GetDev ();
    
    //d3dpp.BackBufferWidth  = viewport->Width  ();
    //d3dpp.BackBufferHeight = viewport->Height ();
    // ... Reset device.

    float fAspectRatio = viewport->AspectRatio ();
    float fFieldOfView = D3DX_PI / 4.0f;
    float fNearPlane   = 10.0f;
    float fFarPlane    = 100000.0f;

    D3DXMATRIX matProjection;

    D3DXMatrixPerspectiveFovLH (&matProjection, fFieldOfView, fAspectRatio, fNearPlane, fFarPlane);
    dev->SetTransform          (D3DTS_PROJECTION, &matProjection);

    /*
    D3DVIEWPORT9 vp = { viewport->X     (),  viewport->Y      (),
                        viewport->Width (),  viewport->Height (),
                        0.0f,               1.0f };
    dev->SetViewport (&vp);
    */

    dev->SetFVF (D3DFVF_VERTEX_BUFFER);
  }
#endif
}


#ifdef HAS_D3D
#pragma comment (lib, "d3d9.lib")
#pragma comment (lib, "d3dx9.lib")

bool
e3dDirect3D::RenderContext::create (eWindow* win, e3dRenderContext* base)
{
  _base = base;
  hWnd  = eHWND (win->getNativeWindowHandle ());

  if (! d3d) {
    d3d = Direct3DCreate9 (D3D_SDK_VERSION);
  }

  d3d->GetDeviceCaps (D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);

  if (! e3dDirect3D::dev)
    e3dDirect3D::dev = new e3dDirect3D::Device ();


  ZeroMemory (&d3dpp, sizeof (d3dpp));

  d3dpp.PresentationInterval   = D3DPRESENT_INTERVAL_IMMEDIATE;
  d3dpp.SwapEffect             = D3DSWAPEFFECT_DISCARD;
  d3dpp.EnableAutoDepthStencil = TRUE;
  d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
  d3dpp.Flags                  = D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL | 
                                 D3DPRESENTFLAG_DEVICECLIP;

  /// TODO: Implement Fullscreen Mode
  bool Windowed = true;

  if (Windowed) {
    d3dpp.Windowed = TRUE;

    // Use the current display mode.
    D3DDISPLAYMODE mode;
    d3d->GetAdapterDisplayMode (D3DADAPTER_DEFAULT , &mode);

    e3dDirect3D::dev->width  = mode.Width;
    e3dDirect3D::dev->height = mode.Height;
    e3dDirect3D::dev->format = D3DFMT_UNKNOWN;//mode.Format; // D3DFMT_X8R8G8B8
  } else {
    /*
    d3dpp.Windowed = FALSE;

    e3dDirect3D::dev->width  = win->getWidth ();
    e3dDirect3D::dev->height = win->getHeight ();
    e3dDirect3D::dev->format = D3DFMT_X8R8G8B8;//mode.Format;
    */
  }

  d3dpp.BackBufferWidth  = e3dDirect3D::dev->width;
  d3dpp.BackBufferHeight = e3dDirect3D::dev->height;
  d3dpp.BackBufferFormat = e3dDirect3D::dev->format;

  d3dpp.hDeviceWindow    = hWnd;

  if (! e3dDirect3D::dev->dev) {
//#define SOFTWARE
//#define NV_PERF_HUD
#ifdef NV_PERF_HUD
    if (FAILED (d3d->CreateDevice (d3d->GetAdapterCount () - 1, D3DDEVTYPE_REF, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING,
                                   &d3dpp, &e3dDirect3D::dev->dev))) {
#else
#ifndef SOFTWARE
    if (FAILED (d3d->CreateDevice (D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                                   D3DCREATE_HARDWARE_VERTEXPROCESSING,
                                   &d3dpp, &e3dDirect3D::dev->dev))) {
#else
      if (FAILED (d3d->CreateDevice (D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd,
                                     D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                     &d3dpp, &e3dDirect3D::dev->dev))) {
#endif
#endif
      Assert (false, _T ("Crap!"));//SAFE_RELEASE (d3d);
      return false;
    }
  }

  LPDIRECT3DDEVICE9 dev = e3dDirect3D::GetDev ();

  dev->CreateAdditionalSwapChain (&d3dpp, &swapchain);

  dev->SetRenderState (D3DRS_AMBIENT,  RGB (255, 255, 255));
  dev->SetRenderState (D3DRS_LIGHTING, false);
  dev->SetRenderState (D3DRS_CULLMODE, D3DCULL_CW);
  dev->SetRenderState (D3DRS_ZENABLE,  D3DZB_TRUE);

  return true;
}

void
e3dDirect3D::RenderContext::destroy (void)
{
  /*
  dev->Release ();
  dev = NULL;
  */
}

#define RequireSwapchain() {                                                 \
  if (! swapchain) {                                                         \
    Assert (swapchain != NULL, _T ("The render context has no swapchain!")); \
    return;                                                                  \
  }                                                                          \
}

#define RequireSwapchainEx(val) {                                            \
  if (! swapchain) {                                                         \
    Assert (swapchain != NULL, _T ("The render context has no swapchain!")); \
    return val;                                                              \
  }                                                                          \
}

void
e3dDirect3D::RenderContext::begin_frame (void)
{
  RequireSwapchain ();

  LPDIRECT3DDEVICE9  dev   = GetDev ();
  LPDIRECT3DSURFACE9 pBack = NULL;

  swapchain->GetBackBuffer (0, D3DBACKBUFFER_TYPE_MONO, &pBack);
  dev->SetRenderTarget     (0, pBack);

  pBack->Release ();
  
  float* ClearColor = _base->GetClearColor ();

  dev->Clear      (0, NULL, D3DCLEAR_TARGET |
                            D3DCLEAR_ZBUFFER,
                              D3DCOLOR_COLORVALUE (ClearColor [0],
                                                   ClearColor [1],
                                                   ClearColor [2],
                                                   ClearColor [3]),
                                _base->GetClearDepth     (),
                                  _base->GetClearStencil ());
  dev->BeginScene ();
}

void
e3dDirect3D::RenderContext::end_frame (void)
{
  RequireSwapchain ();

  LPDIRECT3DDEVICE9 dev = GetDev ();

  dev->EndScene ();

  swapchain->Present (NULL, NULL, hWnd, NULL, 0);
}



/// NOTE: This assumes the backbuffer is 32 bpp, and the x and y
//        coordinates are only used if the num != -1.
e3dPixelBuffer*
e3dDirect3D::RenderContext::get_pixels (int x, int y, int num)
{
  RequireSwapchainEx (NULL);

  LPDIRECT3DDEVICE9  dev   = GetDev ();
  LPDIRECT3DSURFACE9 pSurf = NULL;
  LPDIRECT3DSURFACE9 pBack = NULL;

  dev->CreateOffscreenPlainSurface (d3dpp.BackBufferWidth, 
                                    d3dpp.BackBufferHeight,
                                    d3dpp.BackBufferFormat,
                                    D3DPOOL_SYSTEMMEM, 
                                    &pSurf, NULL);

  e3dPixelBuffer* out = new e3dPixelBuffer ();

  swapchain->GetBackBuffer (0, D3DBACKBUFFER_TYPE_MONO, &pBack);
  dev->GetRenderTargetData (pBack, pSurf);

  pBack->Release ();

  D3DSURFACE_DESC desc;
  D3DLOCKED_RECT  locked;

  pSurf->GetDesc (&desc);

  out->bpp  = 4;
  out->w    = desc.Width;
  out->h    = desc.Height;
  
  if (num == -1)
    out->data = new int [out->w * out->h];
  else
    out->data = new int;

  pSurf->LockRect (&locked, NULL, D3DLOCK_READONLY);
    if (num == -1) {
      memcpy (out->data, locked.pBits, out->bpp * out->w * out->h);
    } else {
      memcpy (out->data, (void *)((int *)(locked.pBits) + (y * out->w) + x), out->bpp);
    }
  pSurf->UnlockRect ();

  pSurf->Release ();

  return out;
}

void
e3dDirect3D::RenderContext::resize (int width, int height)
{
  return;
  /// Resize the swapchain backbuffer
  if (d3dpp.BackBufferWidth  != width ||
      d3dpp.BackBufferHeight != height) {
    Assert (width  <= dev->width &&
            height <= dev->height,
              _T ("RenderContext Resize would require a device reset!\n"));

    if (swapchain) {
      swapchain->Release ();
    }

    swapchain = NULL;

    // Don't allow the backbuffer dimensions to exceed the dev's dimensions.
    //
    //  * Also ensure that the width / height is never 0
    d3dpp.BackBufferWidth  = max (1, min (dev->width,  width));
    d3dpp.BackBufferHeight = max (1, min (dev->height, height));
    d3dpp.BackBufferCount  = 0;

    HRESULT hRet = GetDev ()->CreateAdditionalSwapChain (&d3dpp, &swapchain);

    Assert (swapchain != NULL, _T ("Could not create the requested swapchain!"));

    width  = d3dpp.BackBufferWidth;
    height = d3dpp.BackBufferHeight;
  }
}

e3dTexture*
e3dDirect3D::RenderContext::create_texture_2d (eImage* img, bool mipmap)
{
  Epsilon3D::Texture2D* tex = new
    Epsilon3D::Direct3D::Texture2D ();

  tex->create (img, mipmap);

  return new e3dTexture (tex);
}

e3dVertexCache*
e3dDirect3D::RenderContext::create_vertex_cache (void)
{
  e3dVertexBuffer* vb = new
    e3dDirect3D::VertexBuffer ();

  return new e3dVertexCache (vb);
}

e3dIndexCache*
e3dDirect3D::RenderContext::create_index_cache (void)
{
  e3dIndexBuffer* ib = new
    e3dDirect3D::IndexBuffer ();

  return new e3dIndexCache (ib);
}
#endif
